﻿<template>
  <div class="global-search-container">
    <el-tooltip
      :offset="100"
      placement="bottom"
      popper-class="global-search__tooltip"
    >
      <div slot="content" class="content">
        <p>输入名称、拼音全拼和简拼查找菜单/功能，还可以通过订单号查询订单。</p>
        <p>在任何时候按下 S 键唤醒搜索，按下 ESC 关闭。</p>
      </div>
      <!-- fake是为了解决动画导致popper宽度不能自适应的问题 -->
      <el-autocomplete
        v-show="fakeVisible"
        ref="fakeAutocomplete"
        :fetch-suggestions="(s, cb) => cb([])"
        placeholder="搜索"
        suffix-icon="el-icon-search"
        hide-loading
        class="global-search"
        :class="{focus, expanded}"
        @focus="onFocus"
      />
    </el-tooltip>
    <el-autocomplete
      v-show="visible"
      ref="autocomplete"
      v-model="searchText"
      :fetch-suggestions="fetchSuggestions"
      :debounce="0"
      placeholder="搜索"
      suffix-icon="el-icon-search"
      highlight-first-item
      popper-class="global-search__popper"
      class="global-search"
      :class="{focus, expanded}"
      @focus="onFocus"
      @blur="onBlur"
      @select="onSelect"
    >
      <template slot-scope="{ item }">
        <menu-item v-if="item.path" :data="item" />
        <div v-else>{{item.title}}</div>
      </template>
    </el-autocomplete>
  </div>
</template>

<script>
import MenuItem from './MenuItem';
import { savePageActivityRecord } from '../page-activity-recorder';

export default {
  components: {
    MenuItem
  },
  data() {
    return {
      visible: false,
      fakeVisible: true,
      focus: false,
      expanded: false,
      items: [],
      searchText: ''
    };
  },
  mounted() {
    // 设置pinyin库缓存
    import('pinyin'); 

    // 设置popper高度
    document.querySelector('.global-search__popper .el-autocomplete-suggestion__wrap').style.maxHeight = (34 * 10 + 36) + 'px';

    document.addEventListener('click', this.onClick = (event) => {
      // 如果是CustomEvent则表示是来自iframe的点击事件而不能让popper自动关闭，只能手动
      if (event instanceof CustomEvent) {
        this.$refs.autocomplete.close();
      }
    });

    document.addEventListener('keydown', this.onKeyDown = (event) => {
      event = event instanceof CustomEvent ? event.detail : event;
      //可能是来自iframe的CustomEvent
      const keyCode = event.keyCode;
      if (keyCode == 'S'.charCodeAt()) {
        // 主要排除输入类型（Input、Textarea等）的元素
        // 此处必须比较字符串，因为iframe.HTMLElement和top.HTMLElement引用不等
        const srcElType = event.srcElement.constructor.name;
        if (srcElType == 'HTMLBodyElement' || srcElType =='HTMLDocument') {
          this.onWakeup();
        }
      }
      if (keyCode == 27) {
        this.$refs.autocomplete.getInput().blur();
        this.$refs.autocomplete.close();
      }
    });

    this.$once('hook:beforeDestroy', () => {
      document.removeEventListener('click', this.onClick);
      document.removeEventListener('keydown', this.onKeyDown);
    });
  },
  methods: {
    onWakeup() {
      this.$refs.fakeAutocomplete && this.$refs.fakeAutocomplete.focus();
    },
    onFocus() {
      clearTimeout(this.timer);
      this.focus = true;
      this.expanded = true;
      this.searchText = '';
      // 等待展开动画完成
      this.timer = setTimeout(() => {
        this.fakeVisible = false;
        this.visible = true;
        this.$nextTick(() => {
          this.$refs.autocomplete.focus();
        });
      }, 300);
    },
    onBlur() {
      clearTimeout(this.timer);
      this.focus = false;
      this.searchText = '';

      this.timer = setTimeout(() => {
        // 等待popper收缩动画完成
        this.expanded = false;
        this.timer = setTimeout(() => {
          this.fakeVisible = true;
          this.visible = false;
        }, 200);

        // 等待select事件执行之后再close
        // 解决未点击外部元素时的失去焦点时popper没有关闭
        this.$refs.autocomplete.close();
      }, 300);
    },
    onSelect(item) {
      if (item.path) {
        if (item.matched.hideNode && item.matched.hideNode.pagePath) {
         this.pushPage(item.matched.hideNode.pagePath);
        } else {
          let id = item.path[item.path.length - 1].id;
          app.$refs.navMenu.onSelect(id);
        }
      } else if (item.orderNo) {
        if (!this.$auth('/pension_service_center/supervise/service_personnel_management/order')) {
          this.$message.warning('你没有查看订单权限');
          return;
        }
        this.pushPage({
          path: '/shop/order/details',
          params: {
            order: {
              orderno: item.orderNo
            }
          }
        });
      }

      savePageActivityRecord({
        pageKey: '/global-search',
        eventType: 'created'
      });
    },
    async fetchSuggestions(queryString, callback) {
      if (this.items.length == 0) {
        await this.loadMenuItems();
      }

      queryString = queryString.trim();
      
      let results;

      // 重置搜索菜单时设置过的状态
      this.items = this.items.map((item) => ({...item, matched: false}));

      if (queryString) {
        results = this.queryMenus(queryString);
        if (results.length == 0) {
          results = this.queryOrders(queryString);
        }
      } else {
        results =  this.items;
      }

      callback(results);
    },
    async loadMenuItems() {
      const pinyin = (await import('pinyin')).default;

      // 从用户权限树 permissions 构造出路径。
      // 一个路径是一组路径节点 [pathNode1, pathNode2, ...]，
      // 一个路径节点是由一个权限节点转换的，路径的最后一个路径节点可能会包含 hideSubPaths 属性，
      // hideSubPaths 会存储最后一级子菜单权限的子权限的路径，参数 pathRoot 和 ignoreMenu 是为了构造 hideSubPaths 时需要用到的，
      // 函数返回路径数组 [path1, path2, ...]
      function buildPaths(permissions, pathRoot, ignoreMenu) {
        let paths = [];

        for (let permission of permissions || []) {
          if (!ignoreMenu && !permission.isMenu) {
            if (!pathRoot.hideSubPaths)
              pathRoot.hideSubPaths = [];
            for (let path of buildPaths([permission], pathRoot, true)) {
              pathRoot.hideSubPaths.push(path);
            }
            continue;
          }

          let pathNode = {
            id: permission.id,
            text: permission.text,
            pinyins: pinyin(permission.text, {heteronym: true, style: pinyin.STYLE_NORMAL}),
            pagePath: permission.pagePath
          };

          let path = [pathNode];
          let hasSubPath = false;
          if (permission.children) {
            for (let subPath of buildPaths(permission.children, pathNode, ignoreMenu)) {
              paths.push(path.concat(subPath));
              hasSubPath = true;
            }
          }
          if (!hasSubPath) {
            paths.push(path);
          }
        }

        return paths;
      }

      let paths = buildPaths(app.admin.permissions);
      // autocomplete组件需要对象数组
      this.items = paths.map((path) => ({path}));
    },
    // 查询菜单/功能
    queryMenus(queryString) {
      queryString = queryString.toLowerCase();

      const matchNode = (path) => {
        for (let nodeIndex = 0; nodeIndex < path.length; nodeIndex++) {
          const {text, pinyins} = path[nodeIndex];

          // 数据示例
          // text: 中ab心cd/
          // pinyins: [["zhong","chong"], ["ab"], ["xin"], ["cd/"]]

          const hanReg = /[\u4e00-\u9fa5]/; // 是否汉字
          const letterReg = /[a-zA-Z]/;     // 是否字母
          let restIndex = 0;       // 查询字符串的还未匹配的部分开始的索引
          let hanIndex = -1;       // 候选文本中所有汉字的索引
          let lastMatched = false; // 最近匹配成功过一次
          let startIndex;          // 首次匹配成功的字符的索引
          
          // 遍历候选文本中的每个单位
          for (let charIndex = 0; charIndex < text.length; charIndex++) {
            let matched = false; // 成功匹配了单个候选字
            let char = text[charIndex];
            let isHan = hanReg.test(char);

            if (isHan) {
              hanIndex++;
            }
            
            // 查询串与候选文字成功匹配
            if ((isHan ? char : char.toLowerCase()) == queryString[restIndex]) {
              // 继续下个候选字符
              restIndex++;
              matched = true;
            }
            // 查询串与候选文字不匹配
            else {
              // 如果查询串当前字符是字母（可能是拼音的开头字母），而且候选字也是汉字（则它有拼音），则开始匹配拼音
              if (letterReg.test(queryString[restIndex]) && isHan) {
                // 因为可能是多音字需要遍历
                for (let pinyin of pinyins[hanIndex]) { 
                  // 匹配拼音开头字母
                  if (queryString[restIndex] == pinyin[0]) {
                    matched = true;
                    restIndex++;
                    if (restIndex < queryString.length) {
                      // 是否有全拼匹配
                      let index = queryString.indexOf(pinyin, restIndex - 1);
                      // 必须连续匹配：如果匹配成功过一次，则这次必须是未匹配串的开头才算匹配
                      if (lastMatched ? index == restIndex - 1 : index > -1) {
                        restIndex = index + pinyin.length;
                        matched = true;
                      }
                    }
                  } else {
                    // 举个例子queryString='huifang', pinyins=['yong','hu','hui','fang']
                    // 这里可以发现当queryString[0,2]匹配pinyins[1]，但接着发现queryString[2]不匹配pinyins[2]的'h'
                    // 而且此时restIndex=2
                    if (lastMatched) {
                      lastMatched = false;
                      charIndex--;
                      hanIndex--;
                      restIndex = 0;
                      break;
                    }
                  }

                  if (matched) {
                    // 跳过下一个多音字，继续下个候选字符
                    break;
                  }
                }
              }
            }

            if (matched) {
              // 标记为匹配成功过一次
              if (!lastMatched) {
                lastMatched = true;
                startIndex = charIndex;
              }
              // 如果匹配完了
              if (restIndex == queryString.length) {
                return {depth: nodeIndex, startIndex, endIndex: charIndex + 1};
              }
            }

            // 最近匹配成功过一次，但此次不匹配，则是未连续匹配
            if (lastMatched && !matched) {
              break;
            }
          }

          // 还有未匹配成功的部分则失败
          if (restIndex == queryString.length) {
            break;
          }
        }
        return false;
      };

      let results = this.items.filter((item) => {
        let matched = matchNode([...item.path].reverse());
        if (matched) {
          // 同时需要排序，对于非隐藏菜单节点：越接近顶级节点越排后，越接近叶子节点排较前
          // order值越小表示越靠前
          item.order = matched.depth + matched.startIndex;
          // 设置匹配信息
          matched.depth = item.path.length - 1 - matched.depth;
          item.matched = matched;
          return true;
        }
        const hideOrder = item.path.length + 20;
        let hideSubPaths = item.path[item.path.length - 1].hideSubPaths;
        if (hideSubPaths) {
          for (let path of hideSubPaths) {
            let matched = matchNode(path);
            if (matched) {
              // 排序，同上，但隐藏节点排较后
              item.order = hideOrder + matched.depth;
              matched.hideNode = path[matched.depth];
              matched.depth = item.path.length + matched.depth;
              item.matched = matched;
              return true;
            }
          }
        }
        item.matched = false;
        item.order = 1000;
        return false;
      });
      results = results.sort((a, b) => a.order - b.order);

      return results;
    },
    // 查询订单
    queryOrders(queryString) {
      const orderReg = /^[A-Z]\d{16,}$/;
      if (orderReg.test(queryString)) {
        return [
          {title: '查看订单详情 ' + queryString, orderNo: queryString}
        ];
      }
      return [];
    }
  }
}
</script>

<style scoped>
@media screen and (max-width: 1109px) {
  .global-search-container {
    display: none !important;
  }
}

.global-search-container {
  float:left;
  margin-right: 16px;
  line-height: 60px;
}

.global-search {
  width: 260px;
  transition: 0.3s width ease;
}

.global-search.expanded {
  width: 500px;
}

.global-search__tooltip > .content {
  width: 440px;
}
.global-search__tooltip .content > p {
  margin-block-start: 0;
  margin-block-end: 0;
  line-height: 20px;
}

/* input */
.global-search >>> .el-input__inner {
  font-size: 14px;
  background-color: rgba(14, 103, 200, 0.25) !important;
  border: 1px solid rgba(255,255,255,0);
}

/* placeholder */
.global-search >>> .el-input__inner::-webkit-input-placeholder {
  color: rgba(255,255,255,0.8);
}
.global-search >>> .el-input__inner::-moz-input-placeholder {
  color: rgba(255,255,255,0.8);
}
.global-search >>> .el-input__inner::-ms-input-placeholder {
  color: rgba(255,255,255,0.8);
}
.global-search >>> .el-input__inner:focus::-webkit-input-placeholder {
  color:#909399;
}
.global-search >>> .el-input__inner:focus::-moz-input-placeholder {
  color:#909399;
}
.global-search >>> .el-input__inner:focus::-ms-input-placeholder {
  color:#909399;
}

/* icon */
.global-search >>> .el-input__suffix {
  color:rgba(255,255,255,0.8);
  transition: none;
}
.global-search.focus >>> .el-input__suffix {
  color: #606266;
}

.global-search >>> .el-input__icon {
  font-size: 15px;
  line-height: 60px;
}
</style>